iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0

上一篇我們讓 POST 和 GET 接上了資料庫,資料也確實保存下來了。

今天要把剩下的兩個補完:

PUT /tasks/{id} → 修改
DELETE /tasks/{id} → 刪除

做完之後,整個 API 就是完整的資料庫版本了。

先改「找一筆資料」的方式

Day 15 我們寫過一個 find_task():

def find_task(task_id):
    for task in tasks:
        if task["id"] == task_id:
            return task

    api.abort(404, f"找不到 id 為{task_id} 的作業")

那時候是自己用 for 迴圈一筆一筆比對。

現在資料在資料庫裡,就不用自己找了:

def find_task(task_id):
    task = db.session.get(Task, task_id)

    if task is None:
        api.abort(404, f"找不到 id 為{task_id} 的作業")

    return task

db.session.get(Task, task_id)

意思是「用主鍵去 Task 這張表拿一筆資料」,相當於 Day 18 的:

SELECT * FROM task WHERE id = 1;

找不到的時候它會回傳 None,所以我們自己判斷之後回 404。

Flask-SQLAlchemy 其實也有更短的寫法:

task = db.get_or_404(Task, task_id)

它會在找不到的時候自己丟出 404。

不過我們自己寫一個 find_task(),之後想要自訂錯誤訊息比較方便,所以先維持自己判斷的版本。

PUT:修改資料

@api.expect(task_input)
@api.marshal_with(task_response)
def put(self, task_id):
    """修改指定編號的作業"""
    task = find_task(task_id)
    data = api.payload

    task.name = data.get("name", task.name)
    task.completed = data.get("completed", task.completed)

    db.session.commit()

    return task

跟 Day 15 的版本比一下,差別只有兩個:

原本是改字典:task[“name”] = …
現在是改物件的屬性:task.name = …

改完之後多了一行 db.session.commit()

這裡有一個 ORM 很方便、但一開始會覺得有點神奇的地方:

我們並沒有寫任何 UPDATE 語法。

因為 task 是從 session 裡拿出來的物件,SQLAlchemy 會追蹤它有沒有被改過。
我們改了 task.name 之後,commit 的時候它就會自動產生對應的 UPDATE 送進資料庫。

所以修改不用 add(),只要改屬性再 commit 就可以了。

DELETE:刪除資料

@api.response(204, '刪除成功')
def delete(self, task_id):
    """刪除指定編號的作業"""
    task = find_task(task_id)

    db.session.delete(task)
    db.session.commit()

    return '', 204

一樣先找到那筆資料,找不到就 404。

找到之後用 db.session.delete(task) 標記要刪除,再 commit 真的送出去。

跟 Day 15 的 tasks.remove(task) 是同樣的意思,只是這次是對資料庫做。

完整程式

app.py

from flask import Flask
from flask_restx import Api, Resource, fields
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///tasks.db'

db = SQLAlchemy(app)
api = Api(app, title='Task API', version='1.0', description='30 天鐵人賽練習用的作業管理 API')

class Task(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=False)
    completed = db.Column(db.Boolean, nullable=False, default=False)

task_input = api.model('TaskInput', {
    'name': fields.String(required=True, description='作業名稱', example='洗碗'),
    'completed': fields.Boolean(required=True, description='是否完成', example=False),
})

task_response = api.model('TaskResponse', {
    'id': fields.Integer(readonly=True, description='作業編號'),
    'name': fields.String(description='作業名稱'),
    'completed': fields.Boolean(description='是否完成'),
})

def find_task(task_id):
    task = db.session.get(Task, task_id)

    if task is None:
        api.abort(404, f"找不到 id 為{task_id} 的作業")

    return task

@api.route('/tasks')
class TaskList(Resource):
    @api.marshal_list_with(task_response)
    def get(self):
        """取得所有作業"""
        return db.session.scalars(db.select(Task)).all()

    @api.expect(task_input)
    @api.marshal_with(task_response, code=201)
    def post(self):
        """新增一筆作業"""
        data = api.payload

        task = Task(
            name=data.get("name"),
            completed=data.get("completed", False),
        )

        db.session.add(task)
        db.session.commit()

        return task, 201

@api.route('/tasks/<int:task_id>')
@api.param('task_id', '作業編號')
@api.response(404, '找不到作業')
class TaskDetail(Resource):
    @api.marshal_with(task_response)
    def get(self, task_id):
        """取得指定編號的作業"""
        return find_task(task_id)

    @api.expect(task_input)
    @api.marshal_with(task_response)
    def put(self, task_id):
        """修改指定編號的作業"""
        task = find_task(task_id)
        data = api.payload

        task.name = data.get("name", task.name)
        task.completed = data.get("completed", task.completed)

        db.session.commit()

        return task

    @api.response(204, '刪除成功')
    def delete(self, task_id):
        """刪除指定編號的作業"""
        task = find_task(task_id)

        db.session.delete(task)
        db.session.commit()

        return '', 204

with app.app_context():
    db.create_all()

if __name__ == '__main__':
    app.run(debug=True)

可以跟 Day 16 的版本放在一起看,會發現 API 的架構完全沒變。

變的只有中間那一段:從操作 List 變成操作資料庫。

完整測試一次

python app.py

打開

http://127.0.0.1:5000/
https://ithelp.ithome.com.tw/upload/images/20261006/20183865hDt8rhT4XS.png
Swagger 首頁,五個 API 都在

  1. POST /tasks 新增「洗碗」
  2. POST /tasks 新增「寫 Day 22」
  3. GET /tasks 看到兩筆
  4. PUT /tasks/1 把 completed 改成 true
    https://ithelp.ithome.com.tw/upload/images/20261006/20183865itHFLdgb6V.png

PUT /tasks/1 之後,completed 變成 true

  1. GET /tasks/1 確認真的改了
  2. DELETE /tasks/2

https://ithelp.ithome.com.tw/upload/images/20261006/20183865uqCSpUbfac.png
DELETE /tasks/2 回傳 204

  1. GET /tasks 只剩下一筆

https://ithelp.ithome.com.tw/upload/images/20261006/20183865qSCxP0yBeE.png
刪除後只剩一筆

  1. 最後關掉 Server 再重開,GET /tasks 資料還在
    https://ithelp.ithome.com.tw/upload/images/20261006/20183865qSCxP0yBeE.png
    也可以再用 check_db.py 直接看資料庫,確認裡面真的只剩下一筆、而且 completed 變成 1:
python check_db.py

https://ithelp.ithome.com.tw/upload/images/20261006/20183865mDuyRBuSOz.png

小延伸:用條件查詢

Day 18 有學到 SQL 的 WHERE:

SELECT * FROM task WHERE completed = 0;

用 ORM 也可以做到。

例如讓 GET /tasks 支援「只看還沒完成的作業」:

from flask import request

@api.route('/tasks')
class TaskList(Resource):
    @api.param('completed', '篩選是否完成,填 true 或 false')
    @api.marshal_list_with(task_response)
    def get(self):
        """取得所有作業,可以用 completed 篩選"""
        stmt = db.select(Task)

        completed = request.args.get('completed')
        if completed is not None:
            stmt = stmt.filter_by(completed=(completed.lower() == 'true'))

        return db.session.scalars(stmt).all()

這裡用到的是 Day 13 提過的另一種傳資料方式:Query String。

也就是把條件放在網址上:

/tasks?completed=false

加上 @api.param 之後,Swagger 上的 GET /tasks 也會多一個 completed 輸入框可以填。
https://ithelp.ithome.com.tw/upload/images/20261006/20183865KiGclKRbJv.png
GET /tasks 多出 completed 參數,填 false 之後只查到未完成的作業

(這段是延伸,覺得太多可以先跳過,不影響後面的內容。)

今天完成了:

把 find_task() 改成用 db.session.get() 去資料庫找
PUT:改物件屬性 + commit,SQLAlchemy 自動產生 UPDATE
DELETE:db.session.delete() + commit
整個 CRUD 都接上資料庫了
順便看了怎麼用條件查詢

到這裡,API 加資料庫的基本流程算是完整了。

不過還記得 Day 20 最後提到的限制嗎?

db.create_all() 只會建立還不存在的表。

如果之後想在 Task 多加一個欄位,例如「截止日期」,資料庫並不會自動跟著改,而且直接砍掉資料庫重建又會把資料全部弄丟。

下一篇就來認識 Flask-Migrate,看看資料表改了要怎麼辦。


上一篇
Day 21|POST 真的存進資料庫
下一篇
Day 23|資料表改了怎麼辦?
系列文
從零開始的後端開發:用 Flask 實作 REST API,搞懂 API 與資料庫之間如何協作 共 23 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言